<?php
defined('SYS_RUN') or exit();

class SocketServer {

    const TIMEOUT_READ = 10;

    const TIMEOUT_WRITE = 10;

    const STREAM_LIVE_TIMEOUT = 60;

    const PACKET_SIZE = 512;

    const STREAM_END_LINE = '\n\r';

    const STATUS_SERVER_READING = 'SERVER_READING';

    const STATUS_SERVER_START_SEND = 'SERVER_START_SEND';

    const STATUS_SERVER_END_SEND = 'SERVER_END_SEND';

    const STATUS_CLIENT_READING = 'CLIENT_READING';

    const STATUS_CLIENT_END_SEND = 'CLIENT_END_SEND';

    const STATUS_CLIENT_GOODBYE = 'CLIENT_GOODBYE';

    private $_settings;

    private $_isRun = false;

    const ACTION_PATTERN = 'action%s';

    function __construct($params = null) {
        $configName = 'default';
        $this->_settings = Settings::get('socket');

        if(!is_null($params) && isset($this->_settings[$params['configuration']]))
            $configName = $params['configuration'];
        $this->_settings = $this->_settings[$configName];
    }

    public function start() {
        if($this->_isRun)
            return false;
        else $this->_isRun = true;

        $NULL           = NULL;
        $clientSockets = array();
        $clientSocketsData = array();
        $master = stream_socket_server('tcp://'.$this->_settings['address'].':'.$this->_settings['port'], $number, $string);

        if(!$master) {
            throw new Exception('Can\'t stream socket '.$this->_settings['address'].': '.$this->_settings['port'].'\n');
        }

        stream_set_blocking($master, false);
        stream_set_timeout($master, 0);
        Debug::writeToConsole('Server started at: '.$this->_settings['address'].':'.$this->_settings['port']);
        $read = array($master);
        while($this->_isRun) {
            $socketChanged = stream_select($read, $NULL, $NULL, 0, 10);
            if ($socketChanged) {
                if(in_array($master, $read) && count($clientSockets) < $this->_settings['maxClients']) {
                    $guid = Randomize::generateGuid();
                    $connection = @stream_socket_accept($master);
                    $clientSockets[$guid] = $connection;
                    $clientSocketsData[$guid] = array(
                        'inputBuffer'       => '',
                        'outputBuffer'      => null,
                        'startTime'         => time(),
                        'sendCounter'       => 0
                    );
                    $this->_write($connection, self::STATUS_SERVER_READING);
                    Debug::writeToConsole('Connect new client. Count: '.count($clientSockets));
                }

                foreach($clientSockets as $key => $client) {
                    if(in_array($client, $read)) {
                        $input = stream_get_line($client, 0, self::STREAM_END_LINE);
                        $connectData = &$clientSocketsData[$key];

                        switch ($input) {
                            case self::STATUS_CLIENT_END_SEND:
                                $outputBuffer = $this->_runHandler($connectData['inputBuffer']);
                                $connectData['outputBuffer'] = $this->_prepareOutput($outputBuffer);
                                $this->_write($client, self::STATUS_SERVER_START_SEND);
                                break;
                            case self::STATUS_CLIENT_READING:
                                if(!is_null($connectData['outputBuffer'])) {
                                    $outputPath = substr($connectData['outputBuffer'], $connectData['sendCounter'], self::PACKET_SIZE);
                                    if($outputPath) {
                                        $this->_write($client, $outputPath);
                                        $connectData['sendCounter'] += self::PACKET_SIZE;
                                    } else {
                                        $connectData['sendCounter'] = 0;
                                        $this->_write($client, self::STATUS_SERVER_END_SEND);
                                    }
                                }
                                break;
                            case self::STATUS_CLIENT_GOODBYE:
                                Debug::writeToConsole('Client say: Good bye!');
                                $this->_write($connection, self::STATUS_SERVER_READING);
                                break;
                            default:
                                if($input) {
                                    $connectData['inputBuffer'] = $input;
                                    $this->_write($client, self::STATUS_SERVER_READING);
                                }
                                break;
                        }
                        if((time() - $connectData['startTime']) > self::STREAM_LIVE_TIMEOUT) {
//                            fclose($client);
                            unset($clientSockets[$key]);
                            unset($clientSocketsData[$key]);
                            Debug::writeToConsole('Disconnect client. ID:'.$key.' Count: '.count($clientSockets));
                            break;
                        }
                    }
                }
            }

            $read = $clientSockets;
            $read[] = $master;
            usleep (50);
        }
    }

    public function stop() {
        $this->_isRun = false;
    }

    private function _write($connection, $data) {
        Debug::writeToConsole($data);
        return fwrite($connection, $data.self::STREAM_END_LINE, (self::PACKET_SIZE + strlen(self::STREAM_END_LINE)));
    }

    private function _prepareInput($input) {
        return @unserialize(base64_decode($input));
    }

    public function _prepareOutput($data) {
        return @base64_encode(serialize($data));
    }

    private function _runHandler($data) {
        $response = Lib::get('response.RCli');

        $data = $this->_prepareInput($data);

        if(empty($data))
            return $response->setAnswer(Response::STATUS_INCORRECT_FORMAT, 'Request data is empty!')->getResponse();
        if(!$data || !is_array($data) || is_null($data))
            return $response->setAnswer(Response::STATUS_INCORRECT_FORMAT, 'Incorrect request data!')->getResponse();
        if(!isset($data['controller']))
            return $response->setAnswer(Response::STATUS_INCORRECT_FORMAT, 'Not set controller name!')->getResponse();
        if(!isset($data['action']))
            return $response->setAnswer(Response::STATUS_INCORRECT_FORMAT, 'Not set handler name!')->getResponse();
        $params = ($data['data']) ? $data['data'] : array();
        if(!is_array($params)) $params = array($params);
        $handler = $this->_prepareHandler($data['action']);

        Lib::get('request')->set($params);
        try {
            Controller::get(ucfirst($data['controller']))->$handler($params);
            return $response->setStatus(Response::STATUS_SUCCESS)->getResponse();
        } catch(SocketException $e) {
            return $response->setAnswer(Response::STATUS_FAILED, $e->getMessage())->getResponse();
        }
    }

    private function _prepareHandler($name) {
        return sprintf(self::ACTION_PATTERN, ucfirst($name));
    }
}